package space.edge.d.demo.qrcode.privatecode.encoder;

import space.edge.d.demo.qrcode.common.BitArray;
import space.edge.d.demo.qrcode.common.CharacterSetECI;
import space.edge.d.demo.qrcode.common.EncodeHintType;
import space.edge.d.demo.qrcode.common.ErrorCorrectionLevel;
import space.edge.d.demo.qrcode.privatecode.decoder.Mode;
import space.edge.d.demo.qrcode.privatecode.decoder.Version;
import space.edge.d.demo.qrcode.exception.WriterException;

import java.io.UnsupportedEncodingException;
import java.util.Map;

/**
 * Created by gsl on 2017/9/26.
 */

public class Encoder {

    // The original table is defined in the table 5 of JISX0510:2004 (p.19).
    private static final int[] ALPHANUMERIC_TABLE = {
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,  // 0x00-0x0f
            -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,  // 0x10-0x1f
            36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43,  // 0x20-0x2f
            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1,  // 0x30-0x3f
            -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,  // 0x40-0x4f
            25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1,  // 0x50-0x5f
    };

    private static final String DEFAULT_BYTE_MODE_ENCODING = "UNKNOWN";

    private static final int VERSION_INFO_POLY = 0x1f25;  // 1 1111 0010 0101
    private static final int TYPE_INFO_POLY = 0x537;
    private static final int TYPE_INFO_MASK_PATTERN = 0x5412;

    private Encoder() {
    }

    public static Code encode(String contents, ErrorCorrectionLevel level, Map<EncodeHintType, ?> hints) throws WriterException {

        // 获取源数据编码
        String encoding = DEFAULT_BYTE_MODE_ENCODING;
        boolean hasEncodingHints = hints != null && hints.containsKey(EncodeHintType.CHARACTER_SET);
        if (hasEncodingHints) {
            encoding = hints.get(EncodeHintType.CHARACTER_SET).toString();
        }

        // 获取源数据类型
        Mode mode = chooseMode(contents);

        //构建头部
        BitArray headerBits = new BitArray();
        if (mode == Mode.BYTE && (hasEncodingHints || DEFAULT_BYTE_MODE_ENCODING.equals(encoding))) {
            CharacterSetECI eci = CharacterSetECI.getCharacterSetECIByName(encoding);
            if (eci != null) {
                appendECI(headerBits, eci);
            }
        }

        appendMode(headerBits, mode);

        //构建数据体
        BitArray dataBits = new BitArray();
        appendBytes(contents, mode, dataBits, encoding);


        // 计算数据长度
        int lengthInfo = mode == Mode.BYTE ? dataBits.getSizeInBytes() : contents.length();

        return encode(mode, level, headerBits, dataBits, lengthInfo, hints);
    }

    public static Code encode(byte[] bytes, ErrorCorrectionLevel level, Map<EncodeHintType, ?> hints) throws WriterException {
        Mode mode = Mode.BYTE;
        BitArray headerBits = new BitArray();
        appendMode(headerBits, mode);

        BitArray dataBits = new BitArray();
        append8BitBytes(bytes, dataBits);

        return encode(mode, level, headerBits, dataBits, dataBits.getSizeInBytes(), hints);
    }

    private static Code encode(Mode mode, ErrorCorrectionLevel level, BitArray headerBits, BitArray dataBits, int lengthInfo, Map<EncodeHintType, ?> hints) throws WriterException {

        // 关于Version的逻辑需要修改，因为设计的码型，不容易扩展或缩减数据，所以可能是不同的版本对应的数据量是相同的
        // 但是Version字段是必要的
        Version version;
        if (hints != null && hints.containsKey(EncodeHintType.VERSION)) {
            int versionNum = Integer.parseInt(hints.get(EncodeHintType.VERSION).toString());
            version = Version.getVersionForNumber(versionNum);
            int bitsNeeded = calculateBitsNeeded(mode, headerBits, dataBits);
            if (!willFit(bitsNeeded, version, level)) {
                throw new WriterException("Data too big for requested version");
            }
        } else {
            version = recommendVersion(level, mode, headerBits, dataBits);
        }

        BitArray headerAndDataBits = new BitArray();
        headerAndDataBits.appendBitArray(headerBits);
        appendLengthInfo(lengthInfo, mode, headerAndDataBits);
        headerAndDataBits.appendBitArray(dataBits);

        Version.ECBlocks ecBlocks = version.getECBlocksForLevel(level);
        int numDataBytes = version.getTotalCodewords() - ecBlocks.getTotalECCodewords();

        // Terminate the bits properly.
        terminateBits(numDataBytes, headerAndDataBits);

        // Interleave data bits with error correction code.
        BitArray finalBits = InterleaveData.interleaveWithECBytes(headerAndDataBits,
                version.getTotalCodewords(),
                numDataBytes,
                ecBlocks.getNumBlocks());

        int maskPattern = chooseMaskPattern(finalBits, level);
        BitArray maskedBits = PMaskUtil.maskData(finalBits, maskPattern);
        // TODO: 2017/10/24 计算最合适的mask pattern

         BitArray typeInfoBits = new BitArray();
        makeTypeInfoBits(level, maskPattern, typeInfoBits);

        BitArray versionInfoBits = new BitArray();
        makeVersionInfoBits(version, versionInfoBits);

        Code code = new Code();
        code.setECLevel(level);
        code.setFinalBits(finalBits);
        code.setMode(mode);
        code.setVersion(version);
        code.setTypeInfoBits(typeInfoBits);
        code.setVersionInfoBits(versionInfoBits);
        code.setMaskedBits(maskedBits);
        return code;
    }


    /**
     * 如果数据量没到达到版本的数据容量，则填充数据
     * 并且如果没有8bit对齐，则填充数据
     */
    private static void terminateBits(int numDataBytes, BitArray bits) throws WriterException {
        int capacity = numDataBytes * 8;
        if (bits.getSize() > capacity) {
            throw new WriterException("data bits cannot fit in the Code" + bits.getSize() + " > " +
                    capacity);
        }
        for (int i = 0; i < 4 && bits.getSize() < capacity; ++i) {
            bits.appendBit(false);
        }
        // 如果不是8bit对齐，则填充数据
        int numBitsInLastByte = bits.getSize() & 0x07;//这里就是求8的余数，因为如果为8的整数倍的话，后三位肯定不为1
        if (numBitsInLastByte > 0) {
            for (int i = numBitsInLastByte; i < 8; i++) {
                bits.appendBit(false);
            }
        }
        // 未达到对大数据量则填充信息
        int numPaddingBytes = numDataBytes - bits.getSizeInBytes();
        for (int i = 0; i < numPaddingBytes; ++i) {
            bits.appendBits((i & 0x01) == 0 ? 0xEC : 0x11, 8);
        }
        if (bits.getSize() != capacity) {
            throw new WriterException("Bits size does not equal capacity");
        }
    }


    /**
     * 计算码数据长度：首部+长度信息+源数据长度
     */
    private static int calculateBitsNeeded(Mode mode,
                                           BitArray headerBits,
                                           BitArray dataBits) {
        return headerBits.getSize() + mode.getCharacterCountBits() + dataBits.getSize();
    }

    /**
     * @return true if the number of input bits will fit in a code with the specified version and
     * error correction level.
     */
    private static boolean willFit(int numInputBits, Version version, ErrorCorrectionLevel ecLevel) {
        // In the following comments, we use numbers of Version 7-H.
        // numBytes = 196
        int numBytes = version.getTotalCodewords();
        // getNumECBytes = 130
        Version.ECBlocks ecBlocks = version.getECBlocksForLevel(ecLevel);
        int numEcBytes = ecBlocks.getTotalECCodewords();
        // getNumDataBytes = 196 - 130 = 66
        int numDataBytes = numBytes - numEcBytes;
        int totalInputBytes = (numInputBits + 7) / 8;
        return numDataBytes >= totalInputBytes;
    }

    /**
     * Decides the smallest version of QR code that will contain all of the provided data.
     *
     * @throws WriterException if the data cannot fit in any version
     */
    private static Version recommendVersion(ErrorCorrectionLevel ecLevel,
                                            Mode mode,
                                            BitArray headerBits,
                                            BitArray dataBits) throws WriterException {
        // Hard part: need to know version to know how many bits length takes. But need to know how many
        // bits it takes to know version. First we take a guess at version by assuming version will be
        // the minimum, 1:
        // Use that guess to calculate the right version. I am still not sure this works in 100% of cases.
        int bitsNeeded = calculateBitsNeeded(mode, headerBits, dataBits);
        return chooseVersion(bitsNeeded, ecLevel);
    }

    private static Version chooseVersion(int numInputBits, ErrorCorrectionLevel ecLevel) throws WriterException {
        for (int versionNum = 1; versionNum <= 1; versionNum++) {
            Version version = Version.getVersionForNumber(versionNum);
            if (willFit(numInputBits, version, ecLevel)) {
                return version;
            }
        }
        throw new WriterException("Data too big");
    }

    /**
     * Choose the best mode by examining the content. Note that 'encoding' is used as a hint;
     * if it is Shift_JIS, and the input is only double-byte Kanji, then we return {@link Mode#KANJI}.
     */
    private static Mode chooseMode(String content) {
        boolean hasNumeric = false;
        boolean hasAlphanumeric = false;
        for (int i = 0; i < content.length(); ++i) {
            char c = content.charAt(i);
            if (c >= '0' && c <= '9') {
                hasNumeric = true;
            } else if (getAlphanumericCode(c) != -1) {
                hasAlphanumeric = true;
            } else {
                return Mode.BYTE;
            }
        }
        if (hasAlphanumeric) {
            return Mode.ALPHANUMERIC;
        }
        if (hasNumeric) {
            return Mode.NUMERIC;
        }
        return Mode.BYTE;
    }

    /**
     * Append "bytes" in "mode" mode (encoding) into "bits". On success, store the result in "bits".
     */
    private static void appendBytes(String content,
                                    Mode mode,
                                    BitArray bits,
                                    String encoding) throws WriterException {
        switch (mode) {
            case NUMERIC:
                appendNumericBytes(content, bits);
                break;
            case ALPHANUMERIC:
                appendAlphanumericBytes(content, bits);
                break;
            case BYTE:
                append8BitBytes(content, bits, encoding);
                break;
            case HANZI:
                break;
            default:
                throw new WriterException("Invalid mode: " + mode);
        }
    }

    /**
     * 如果数据类型为数字则压缩数据
     * 3个数字(最大999)压缩至10位二进制
     * 2个数字(最大99)压缩至7位二进制
     * 1个数字(最大9)压缩至4位二进制
     */
    private static void appendNumericBytes(CharSequence content, BitArray bits) {
        int length = content.length();
        int i = 0;
        while (i < length) {
            int num1 = content.charAt(i) - '0';
            if (i + 2 < length) {
                // Encode three numeric letters in ten bits.
                //
                int num2 = content.charAt(i + 1) - '0';
                int num3 = content.charAt(i + 2) - '0';
                bits.appendBits(num1 * 100 + num2 * 10 + num3, 10);
                i += 3;
            } else if (i + 1 < length) {
                // Encode two numeric letters in seven bits.
                int num2 = content.charAt(i + 1) - '0';
                bits.appendBits(num1 * 10 + num2, 7);
                i += 2;
            } else {
                // Encode one numeric letter in four bits.
                bits.appendBits(num1, 4);
                i++;
            }
        }
    }

    private static void appendAlphanumericBytes(CharSequence content, BitArray bits) throws WriterException {
        int length = content.length();
        int i = 0;
        while (i < length) {
            int code1 = getAlphanumericCode(content.charAt(i));
            if (code1 == -1) {
                throw new WriterException();
            }
            if (i + 1 < length) {
                int code2 = getAlphanumericCode(content.charAt(i + 1));
                if (code2 == -1) {
                    throw new WriterException();
                }
                // Encode two alphanumeric letters in 11 bits.
                bits.appendBits(code1 * 45 + code2, 11);
                i += 2;
            } else {
                // Encode one alphanumeric letter in six bits.
                bits.appendBits(code1, 6);
                i++;
            }
        }
    }

    private static void append8BitBytes(String content, BitArray bits, String encoding)
            throws WriterException {
        byte[] bytes;
        try {
            bytes = content.getBytes(encoding);
        } catch (UnsupportedEncodingException uee) {
            throw new WriterException(uee);
        }
        append8BitBytes(bytes, bits);
    }

    private static void append8BitBytes(byte[] bytes, BitArray bits) {
        for (byte b : bytes) {
            bits.appendBits(b, 8);
        }
    }

    /**
     * @return the code point of the table used in alphanumeric mode or
     * -1 if there is no corresponding code in the table.
     */
    private static int getAlphanumericCode(int code) {
        if (code < ALPHANUMERIC_TABLE.length) {
            return ALPHANUMERIC_TABLE[code];
        }
        return -1;
    }

    private static void appendECI(BitArray bitArray, CharacterSetECI eci) {
        bitArray.appendBits(Mode.ECI.getBits(), 4);
        bitArray.appendBits(eci.getValue(), 8);
    }

    private static void appendMode(BitArray bitArray, Mode mode) {
        bitArray.appendBits(mode.getBits(), 4);
    }

    private static void appendLengthInfo(int length, Mode mode, BitArray headerAndDataBits) throws WriterException {
        int countBits = mode.getCharacterCountBits();
        if (length >= 1 << countBits)
            throw new WriterException(length + " is bigger than " + ((1 << countBits) - 1));
        headerAndDataBits.appendBits(length, countBits);
    }

    // Return the position of the most significant bit set (to one) in the "value". The most
    // significant bit is position 32. If there is no bit set, return 0. Examples:
    // - findMSBSet(0) => 0
    // - findMSBSet(1) => 1
    // - findMSBSet(255) => 8
    // 计算给定值的2进制次幂，向上取值 ceil
    static int findMSBSet(int value) {
        return 32 - Integer.numberOfLeadingZeros(value);
    }

    // Calculate BCH (Bose-Chaudhuri-Hocquenghem) code for "value" using polynomial "poly". The BCH
    // code is used for encoding type information and version information.
    // Example: Calculation of version information of 7.
    // f(x) is created from 7.
    //   - 7 = 000111 in 6 bits
    //   - f(x) = x^2 + x^1 + x^0
    // g(x) is given by the standard (p. 67)
    //   - g(x) = x^12 + x^11 + x^10 + x^9 + x^8 + x^5 + x^2 + 1
    // Multiply f(x) by x^(18 - 6)
    //   - f'(x) = f(x) * x^(18 - 6)
    //   - f'(x) = x^14 + x^13 + x^12
    // Calculate the remainder of f'(x) / g(x)
    //         x^2
    //         __________________________________________________
    //   g(x) )x^14 + x^13 + x^12
    //         x^14 + x^13 + x^12 + x^11 + x^10 + x^7 + x^4 + x^2
    //         --------------------------------------------------
    //                              x^11 + x^10 + x^7 + x^4 + x^2
    //
    // The remainder is x^11 + x^10 + x^7 + x^4 + x^2
    // Encode it in binary: 110010010100
    // The return value is 0xc94 (1100 1001 0100)
    //
    // Since all coefficients in the polynomials are 1 or 0, we can do the calculation by bit
    // operations. We don't care if coefficients are positive or negative.
    static int calculateBCHCode(int value, int poly) {
        if (poly == 0) {
            throw new IllegalArgumentException("0 polynomial");
        }
        // If poly is "1 1111 0010 0101" (version info poly), msbSetInPoly is 13. We'll subtract 1
        // from 13 to make it 12.
        int msbSetInPoly = findMSBSet(poly);
        value <<= msbSetInPoly - 1;
        // Do the division business using exclusive-or operations.
        while (findMSBSet(value) >= msbSetInPoly) {
            value ^= poly << (findMSBSet(value) - msbSetInPoly);
        }
        // Now the "value" is the remainder (i.e. the BCH code)
        return value;
    }

    // Make bit vector of type information. On success, store the result in "bits" and return true.
    // Encode error correction level and mask pattern. See 8.9 of
    // JISX0510:2004 (p.45) for details.
    // (15,5)码长：15，信息位：5，监督位：10
    private static void makeTypeInfoBits(ErrorCorrectionLevel ecLevel, int maskPattern, BitArray bits)
            throws WriterException {
        if (!Code.isValidMaskPattern(maskPattern)) {
            throw new WriterException("Invalid mask pattern");
        }
        int typeInfo = (ecLevel.getBits() << 3) | maskPattern;
        bits.appendBits(typeInfo, 5);

        int bchCode = calculateBCHCode(typeInfo, TYPE_INFO_POLY);
        bits.appendBits(bchCode, 10);

        BitArray maskBits = new BitArray();
        maskBits.appendBits(TYPE_INFO_MASK_PATTERN, 15);
        bits.xor(maskBits);

        if (bits.getSize() != 15) {  // Just in case.
            throw new WriterException("should not happen but we got: " + bits.getSize());
        }
    }

    // Make bit vector of version information. On success, store the result in "bits" and return true.
    // See 8.10 of JISX0510:2004 (p.45) for details.
    // (18,6)码长：18，信息位：6，监督位：12
    private static void makeVersionInfoBits(Version version, BitArray bits) throws WriterException {
        bits.appendBits(version.getVersionNumber(), 6);
        int bchCode = calculateBCHCode(version.getVersionNumber(), VERSION_INFO_POLY);
        bits.appendBits(bchCode, 12);

        if (bits.getSize() != 18) {  // Just in case.
            throw new WriterException("should not happen but we got: " + bits.getSize());
        }
    }

    private static int chooseMaskPattern(BitArray bits, ErrorCorrectionLevel ecLevel) {
        return 0;
    }

}
